﻿// Copyright 2007-2009 Stuart Caborn, Alex Scordellis
//  
//  Licensed under the Apache License, Version 2.0 (the "License"); you may not
//  use this file except in compliance with the License. You may obtain a copy of
//  the License at
//  
//  http://www.apache.org/licenses/LICENSE-2.0
//  
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
//  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
//  License for the specific language governing permissions and limitations under
//  the License.
// 


using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace NSynthesis.Reflection
{
    public static class TypeMatcher
    {
        /// <summary>
        /// Determines if a type could possibly provide an implementation
        /// of the method provided. Does not inspect what methods there are
        /// in the type.
        /// 
        /// In NSynthesis, this is used to find types that could possibly
        /// contain a method that we should demand a unit test for, given
        /// an intercepted call to a mock.
        /// </summary>
        /// <param name="candidate">The type to inspect.</param>
        /// <param name="mockCall">The info that PostSharp gave us when we intercepted
        /// the actual mock call.</param>
        /// <returns>True if candidate could contain a production implementation of mockCall.</returns>
        public static bool CouldImplement(this Type candidate, MethodInfo mockCall)
        {
            if (candidate.IsInterface)
                return false;

            if (candidate.IsGenericType && !candidate.IsGenericTypeDefinition)
                throw new InvalidOperationException(
                    "We haven't supported examining closed generic types (e.g. Class<Foo>). NSynthesis calls this method on the Types it gets from enumerating all the types in an assembly, which will never include this sort of Type.");

            var baseType = mockCall.DeclaringType.RemoveProxy();

            // IsAssignableTo doesn't work as we want it to when generics are involved
            // so we need manual exceptions.
            if (baseType.IsGenericType && candidate.IsGenericTypeDefinition)
                return candidate.IsSubclassOfGenericTypeDefinition(baseType) ||
                       candidate.IsImplementorOfGenericTypeDefinition(baseType);

            return baseType.IsAssignableFrom(candidate);
        }

        // Handles the case where Class<> : IInterface<> could implement a mock call to IInterface<Foo>
        private static bool IsImplementorOfGenericTypeDefinition(this Type candidate, Type baseType)
        {
            if (!baseType.IsInterface)
                return false;

            var implementedInterfaces = from iface in candidate.GetInterfaces()
                                        where iface.IsGenericType && !iface.IsGenericTypeDefinition && iface.GetGenericTypeDefinition().Equals(baseType.GetGenericTypeDefinition())
                                        select iface;

            if (implementedInterfaces.Count() == 0)
                return false;
            if (implementedInterfaces.Count() > 1)
                throw new NotImplementedException("This tricky situation (class Implementation : IBase<T>, IBase<U>) is not yet supported");

            return ImplementedTypeHasMatchingGenericParameters(baseType, candidate, implementedInterfaces.First());
        }

        // Handles the case where Class<> or SubClass<> : Class<> could implement a mock call to Class<Foo>
        private static bool IsSubclassOfGenericTypeDefinition(this Type candidate, Type baseType)
        {
            if (baseType.IsInterface)
                return false;

            var inheritedTypes = from inheritedType in candidate.BaseClassesIncludingSelf()
                                 where inheritedType.IsGenericType && baseType.GetGenericTypeDefinition().Equals(inheritedType.GetGenericTypeDefinition())
                                 select inheritedType;

            if (inheritedTypes.Count() == 0)
                return false;
            if (inheritedTypes.Count() > 1)
                throw new InvalidOperationException(String.Format("We found two types in the hierarchy of {0} that matched {1}. This really shouldn't happen.", candidate, baseType));

            return ImplementedTypeHasMatchingGenericParameters(baseType, candidate, inheritedTypes.First());
        }

        private static bool ImplementedTypeHasMatchingGenericParameters(Type baseType, Type candidate, Type implementedType)
        {
            if (implementedType.GetGenericArguments().Count() != candidate.GetGenericArguments().Count())
            {
                // This is a case like class Impl<T> : IBase<Foo, T> or Impl<T> : Base<Foo,T>
                // Now:
                //  implementedType = IBase<Foo, T> or Base<Foo,T> => implementedType.GetGenericArguments() == [Foo, T]
                //  candidate = Impl<T>                            => candidate.GetGenericArguments() == [T]
                //  baseType = IBase<Foo,Bar> or Base<Foo,Bar>     => baseType.GetGenericArguments() = [Foo,Bar]
                // We are a match if every bound generic argument in implementedInterfaces.First().GetGenericArguments() matches
                // the same positioned generic argument in baseType.GetGenericArguments()
                bool allGenericParametersMatch = true;
                implementedType.GetGenericArguments().EachWithIndex(delegate(Type arg, int i)
                                                                    {
                                                                        if (!arg.IsGenericParameter)
                                                                            if (!baseType.GetGenericArguments()[i].Equals(arg))
                                                                                allGenericParametersMatch = false;
                                                                    });
                return allGenericParametersMatch;
            }

            // This is an easy case like Impl<T> : Base<T> or Impl<T> : IBase<T>
            return true;
        }

        private static IEnumerable<Type> BaseClassesIncludingSelf(this Type type)
        {
            do
            {
                yield return type;
                type = type.BaseType;
            } while (type != null);
        }
    }
}